from dataclasses import dataclass, field, fields
from typing import Any, ClassVar, Dict, Iterable, List, Optional, Protocol, Union
from html import escape
from collections import OrderedDict

class DOMConfig:
    # Specify the indent on rendering for every level.
    INDENT = 2

    SELF_CLOSING_TAGS = set(("area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"))

class CanRender(Protocol):
    def do_render(self, indent: Optional[str]) -> str:  # pragma: no cover
        ...

class _Meta(type):
    """Allow []-access on the class of Nodes."""

    def __getitem__(self, child: Union[Any, Iterable]) -> "Node":
        return self()[child]  # type: ignore

@dataclass
class Node(metaclass=_Meta):
    NAME: ClassVar[Optional[str]] = None
    # dict[str, str | bool]
    attr: Dict[str, Any] = field(default_factory=OrderedDict)
    # list[str | CanRender]
    children: List[Any] = field(default_factory=list)

    def __getitem__(self, child):
        if isinstance(child, Iterable) and not isinstance(child, str):
            self.children = list(child)
        else:
            self.children = [child]

        return self

    @property
    def tag_name(self) -> str:
        return self.NAME or self.__class__.__name__
    
    def __str__(self) -> str:
        return self.render()

    def __repr__(self) -> str:
        return self.render()
    
    def render(self, indent: bool = False) -> str:
        if indent:
            return self.do_render("")
        else:
            return self.do_render(None)
        

    def do_render(self, indent: Optional[str]) -> str:
            indent_str = ""
            new_indent = None
            if indent is not None:
                new_indent = indent + (" " * DOMConfig.INDENT)
                indent_str = indent
            result = f"{indent_str}<{self.tag_name}"
            no_content = len(self.children) == 0

            def indent_newline() -> None:
                nonlocal result
                if indent is not None:
                    result += "\n"

            result += self._render_attr()

            is_self_closing = no_content and self.tag_name in DOMConfig.SELF_CLOSING_TAGS
            if is_self_closing:
                result += " />"
            else:
                result += ">"
            indent_newline()

            result += self._render_content(new_indent)

            if not is_self_closing:
                if self.tag_name == "textarea":
                    indent_str = "" # textarea标签中indent_str会产生额外内容，此处禁止缩进
                result += f"{indent_str}</{self.tag_name}>"
                indent_newline()

            return result

    def _render_content(self, indent: Optional[str]) -> str:
        result = ""

        for child in self.children:
            if isinstance(child, str):
                if isinstance(self, HtmlText):
                    result = child
                else:
                    result += f"{indent or ''}{escape(child)}"
                if indent is not None:
                    result += "\n"
            else:
                if isinstance(child, Iterable):
                    for e in child:
                        result += e.do_render(indent)
                else:
                    result += child.do_render(indent)
        return result

    def _render_attr(self) -> str:
        result = ""
        for field_ in fields(self):
            if field_.name in (
                "tag_name",
                "children",
                "aria_attr",
                "data_attr",
                "attr",
            ):
                continue
            value = getattr(self, field_.name)
            if value is not None and value is not False:
                name = field_.name
                if name.endswith("_"):
                    # e.g. class_ -> class
                    name = name[:-1]
                # e.g. accept_charset -> accept-charset
                name = name.replace("_", "-")
                result += self._render_single_attr(name, value)
        for name, value in self.attr.items():
            result += self._render_single_attr(name, value)

        return result

    def _render_single_attr(self, name: str, value: Union[str, bool]) -> str:
        if isinstance(value, bool):
            if value is True:
                return f" {name}"
            # False will be ignored
            return ""
        else:
            return f' {name}="{value}"'
        
@dataclass
class NoEscape(CanRender):
    """Add string content, that should not be html escaped."""

    content: str

    def do_render(self, indent: Optional[str]) -> str:
        return self.content


@dataclass
class HtmlText(CanRender):
    """Add html content, like innerHTML."""

    content: str

    def do_render(self, indent: Optional[str]) -> str:
        return self.content